﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Dynamic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CafeOnline.MvcFramework.Extensions;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

namespace CafeOnline.MvcFramework.Web.Json {
    public class ObjectOptions : DynamicObject {
        #region SFields

        private static JsonSerializer s_ser;
        #endregion SFields
        #region IFields

        private readonly Dictionary<string, object> m_options;

        #endregion IFields
        #region SConstructors

        static ObjectOptions() {
            s_ser = new JsonSerializer();
            s_ser.Converters.Add(new StringEnumConverter {
                CamelCaseText = false
            });
            s_ser.Converters.Add(new ObjectOptionsConverter {
                CamelCaseText = false
            });
        }

        #endregion SConstructors
        #region IConstructors

        /// <summary>
        /// Initializes a new instance of the <see cref="ObjectOptions"/> class.
        /// </summary>
        public ObjectOptions() {
            m_options = new Dictionary<string, object>();
        }

        #endregion IConstructors
        #region IProperties

        /// <summary>
        /// Gets a value indicating whether this instance is populated. Instance is populated once the method Populate is called. It is used to ensure that all required information provided from the Context is filled.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is populated; otherwise, <c>false</c>.
        /// </value>
        public bool IsPopulated { get; protected set; }

        #endregion IProperties
        #region IMethods

        public void Clear() {
            lock (this) {
                m_options.Clear();
                IsPopulated = false;
            }
        }

        /// <summary>
        /// Populates the instance with the specified context.
        /// </summary>
        /// <param name="context">The context.</param>
        /// <remarks>Any sub OjectOptions or enumerable of ObjectOptions will also be populated. It is possible to override this method in inherited class to provide additional data in the the context.</remarks>
        public virtual void Populate(SerializationContext context) {
            lock (this) {
                IsPopulated = true;
                m_options.ForEach(a => {
                    if (a.Value is ObjectOptions) {
                        var op = (ObjectOptions)a.Value;
                        if (!op.IsPopulated) {
                            op.Populate(context);
                        }
                    } else if (a.Value is IEnumerable) {
                        var list =
                                          ((IEnumerable)a.Value).OfType<ObjectOptions>().Where(b => !b.IsPopulated);
                        list.ForEach(b => b.Populate(context));
                    }
                });
            }
        }


        /// <summary>
        /// Returns the enumeration of all instance member names.
        /// </summary>
        /// <returns>
        /// A sequence that contains instance member names.
        /// </returns>
        public override IEnumerable<string> GetDynamicMemberNames() {
            return m_options.Keys.AsEnumerable();
        }

        /// <summary>
        /// Provides the implementation for operations that get member values. Classes derived from the <see cref="T:System.Dynamic.DynamicObject"/> class can override this method to specify dynamic behavior for operations such as getting a value for a property.
        /// </summary>
        /// <param name="binder">Provides information about the object that called the dynamic operation. The binder.Name property provides the name of the member on which the dynamic operation is performed. For example, for the Console.WriteLine(sampleObject.SampleProperty) statement, where sampleObject is an instance of the class derived from the <see cref="T:System.Dynamic.DynamicObject"/> class, binder.Name returns "SampleProperty". The binder.IgnoreCase property specifies whether the member name is case-sensitive.</param>
        /// <param name="result">The result of the get operation. For example, if the method is called for a property, you can assign the property value to <paramref name="result"/>.</param>
        /// <returns>
        /// true if the operation is successful; otherwise, false. If this method returns false, the run-time binder of the language determines the behavior. (In most cases, a run-time exception is thrown.)
        /// </returns>
        public override bool TryGetMember(GetMemberBinder binder, out object result) {
            bool found = false;
            result = null;
            if (binder.IgnoreCase) {
                var props = from c in m_options
                            where c.Key.Equals(binder.Name, StringComparison.OrdinalIgnoreCase)
                            select c.Value;
                if (props.Any()) {
                    result = props.First();
                    found = true;
                }
            } else {
                found = m_options.TryGetValue(binder.Name, out result);
            }
            if (!found) {
                found = base.TryGetMember(binder, out result);
            }
            return found;
        }

        /// <summary>
        /// Adds the specified member to the instance.
        /// </summary>
        /// <param name="memberName">Member name.</param>
        /// <param name="value">Member value.</param>
        public void Add(string memberName, object value) {
            lock (this) {
                if (m_options.ContainsKey(memberName)) {
                    m_options[memberName] = value;
                } else {
                    m_options.Add(memberName, value);
                }
            }
        }

        /// <summary>
        /// Determines whether the specified instance contains this member.
        /// </summary>
        /// <param name="memberName">The member name.</param>
        /// <returns>
        ///   <c>true</c> if the specified instance contains the member name otherwise, <c>false</c>.
        /// </returns>
        public bool ContainsMember(string memberName) {
            return m_options.ContainsKey(memberName);
        }

        /// <summary>
        /// Gets the member value (<see cref="System.Object"/>) of the specified member name.
        /// </summary>
        public object this[string memberName] {
            get { return m_options[memberName]; }
        }

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// The string returned is a javascript object that can be directly used within an HTML page.
        /// </summary>
        /// <returns>
        /// A <see cref="System.String"/> that represents this instance.
        /// </returns>
        public override string ToString() {
            lock (this) {
                if (!IsPopulated) {
                    Populate(new SerializationContext());
                }
                string res;
                using (var writer = new StringWriter()) {
                    using (var jwr = new JsonTextWriter(writer)) {
                        jwr.QuoteName = false;
                        jwr.QuoteChar = '\'';
                        s_ser.Serialize(jwr, this);
                    }
                    res = writer.ToString();
                }
                return res;
            }
        }

        /// <summary>
        /// ObjectOptionsConverter class replaces recursively ObjectOptions instances with their dictionary content.
        /// This avoids throwing first-chance RuntimeBinderException when searching for dynamic members.
        /// </summary>
        public class ObjectOptionsConverter : JsonConverter {
            public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
                ObjectOptions oo = value as ObjectOptions;
                serializer.Serialize(writer, Convert(oo));
            }

            private Dictionary<string, object> Convert(ObjectOptions objectOptions) {
                Dictionary<string, object> res = new Dictionary<string, object>();
                objectOptions.m_options.ForEach(item => {
                    if (item.Value is ObjectOptions) {
                        res.Add(item.Key, Convert(item.Value as ObjectOptions));
                    } else {
                        res.Add(item.Key, item.Value);
                    }
                });
                return res;
            }


            public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
                return serializer.Deserialize(reader, objectType);
            }

            public override bool CanConvert(Type objectType) {
                return objectType.IsSubclassOf(typeof(ObjectOptions));
            }

            public bool CamelCaseText { get; set; }
        }
        #endregion IMethods
    }
}
